﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.Diagnostics;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Threading.Tasks;
using System.ComponentModel;

namespace MailboxRightsLookup
{
    class ReverseSearcher
    {
        BackgroundWorker _worker;
        Dictionary<string, Dictionary<SecurityIdentifier, List<MailboxPermission>>> _sidDict;

        public ReverseSearcher(BackgroundWorker worker)
        {
            _worker = worker;
        }

        public string CreateIndex()
        {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            _sidDict = new Dictionary<string, Dictionary<SecurityIdentifier, List<MailboxPermission>>>();
            int totalDomains = 0, totalUsers = 0, totalPermissions = 0;

            // Get a root DirectoryEntry to default naming context in every domain
            using (Forest forest = Forest.GetCurrentForest())
            {
                totalDomains = forest.Domains.Count;
                List<string> domainNames = new List<string>(totalDomains);
                List<Task<Dictionary<SecurityIdentifier, List<MailboxPermission>>>> domainTasks =
                    new List<Task<Dictionary<SecurityIdentifier, List<MailboxPermission>>>>(totalDomains);
                foreach (Domain domain in forest.Domains)
                {
                    try
                    {
                        string name = domain.Name;
                        domainNames.Add(name);
                        DirectoryEntry de = domain.GetDirectoryEntry();
                        // Index each domain's Mailbox Permission in separate thread
                        domainTasks.Add(Task<Dictionary<SecurityIdentifier, List<MailboxPermission>>>.Factory.StartNew(
                                () => IndexMailboxPermissions(de)));
                    }
                    catch (Exception ex)
                    {
                        throw new InvalidOperationException(String.Format(
                            "Error initializing mailbox permission indexer for domain {0}", domain.Name), ex);
                    }
                }

                int finishedTasks = 0;
                while (finishedTasks < totalDomains)
                {
                    int finishedTask = Task.WaitAny(domainTasks.ToArray());
                    try
                    {
                        Dictionary<SecurityIdentifier, List<MailboxPermission>> result = domainTasks[finishedTask].Result;
                        _sidDict.Add(domainNames[finishedTask], result);
                        totalUsers += result.Keys.Count;
                        foreach (var list in result.Values)
                            totalPermissions += list.Count;

                        _worker.ReportProgress(++finishedTasks * 100 / totalDomains,
                            String.Format("PLEASE WAIT: Indexed {0} users with {1} permissions in {2} domains in {3:0.000} seconds", totalUsers,
                                totalPermissions, finishedTasks, stopwatch.ElapsedMilliseconds / 1000.0));
                    }
                    catch (Exception ex)
                    {
                        throw new InvalidOperationException(String.Format(
                            "Error gathering results of indexer for domain {0}", domainNames[finishedTask]), ex);
                    }
                    finally
                    {
                        domainTasks.RemoveAt(finishedTask);
                        domainNames.RemoveAt(finishedTask);
                    }
                }

            }
            stopwatch.Stop();
            return String.Format("READY: Found {0} users with {1} permissions in {2} domains in {3:0.000} seconds", totalUsers, 
                totalPermissions, _sidDict.Keys.Count, stopwatch.ElapsedMilliseconds / 1000.0);
        }

        private Dictionary<SecurityIdentifier, List<MailboxPermission>> IndexMailboxPermissions(DirectoryEntry root)
        {
            Dictionary<SecurityIdentifier, List<MailboxPermission>> dict;
            string rootPath = root.Path;
            string filter = "(&(objectClass=user)(homeMDB=*))";

            Debug.Print("Starting index for mailbox permissions in {0}", rootPath);
            
            try
            {
                // NOTE: To read the security descriptor as a LDAP attribute (nTSecurityDescriptor), unless you are running 
                // an admin account, you need to set a SecurityMask in the DirectorySearcher in order to retrieve
                // only the DACL. Only admins can retrieve the SACL part. Otherwise the query silently fails and returns
                // a null value for the nTSecurityDescriptor attribute.
                // More about this in the "The. NET Developer's Guide to Directory Services Programming" book
                // Pages: 54, 66
                string[] properties = new string[] { "objectGUID", "msExchMailboxSecurityDescriptor", "nTSecurityDescriptor" };
                dict = new Dictionary<SecurityIdentifier, List<MailboxPermission>>();
                using (DirectorySearcher searcher = new DirectorySearcher(root, filter, properties))
                {
                    searcher.ReferralChasing = ReferralChasingOption.All;
                    searcher.SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Group | SecurityMasks.Owner;
                    // Set client to timeout after server default time limit
                    searcher.ClientTimeout = new TimeSpan(120 * TimeSpan.TicksPerSecond);
                    using (SearchResultCollection results = searcher.FindAll())
                    {
                        Guid mailboxGuid;
                        MailboxPermission mailboxPermission;
                        Dictionary<SecurityIdentifier, MailboxPermission> resultDict;
                        byte[] securityDescriptor;
                        string sidString;
                        SecurityIdentifier sid;

                        ActiveDirectorySecurity adSecurity = new ActiveDirectorySecurity();
                        foreach (SearchResult result in results)
                        {
                            AuthorizationRuleCollection rules;
                            resultDict = new Dictionary<SecurityIdentifier, MailboxPermission>();
                            mailboxGuid = new Guid((byte[])result.Properties["objectGuid"][0]);

                            // Mailbox Full Access Permission
                            if (result.Properties.Contains("msExchMailboxSecurityDescriptor"))
                            {
                                try
                                {
                                    securityDescriptor = (byte[])result.Properties["msExchMailboxSecurityDescriptor"][0];
                                    adSecurity.SetSecurityDescriptorBinaryForm(securityDescriptor);
                                    rules = adSecurity.GetAccessRules(true, false, typeof(SecurityIdentifier));

                                    foreach (var rule in rules)
                                    {
                                        ActiveDirectoryAccessRule adRule = rule as ActiveDirectoryAccessRule;
                                        if (adRule != null)
                                        {
                                            sidString = adRule.IdentityReference.Value;
                                            sid = new SecurityIdentifier(sidString);
                                            // Skip principal self permissions
                                            if (sidString == "S-1-5-10")
                                                continue;
                                            mailboxPermission = new MailboxPermission(mailboxGuid, rootPath);
                                            if (mailboxPermission.HasFullAccessPermission(adRule))
                                                resultDict.Add(sid, mailboxPermission);
                                        }
                                    }
                                }
                                catch (Exception ex)
                                {
                                    throw new InvalidOperationException(String.Format(
                                        "Error processing msExchMailboxSecurityDescriptor attribute in object {0}", mailboxGuid), ex);
                                }
                            }
                            else
                                Debug.WriteLine(String.Format("Null msExchMailboxSecurityDescriptor attribute in object {0}", mailboxGuid.ToString()));

                            // Send-As Permission
                            //adSecurity = result.GetDirectoryEntry().ObjectSecurity;
                            if (result.Properties.Contains("nTSecurityDescriptor"))
                            {
                                try
                                {
                                    securityDescriptor = (byte[])result.Properties["nTSecurityDescriptor"][0];
                                    adSecurity.SetSecurityDescriptorBinaryForm(securityDescriptor);
                                    rules = adSecurity.GetAccessRules(true, false, typeof(SecurityIdentifier));

                                    foreach (var rule in rules)
                                    {
                                        ObjectAccessRule oaRule = rule as ObjectAccessRule;
                                        if (oaRule != null)
                                        {
                                            sidString = oaRule.IdentityReference.Value;
                                            sid = new SecurityIdentifier(sidString);
                                            // Skip principal self permissions
                                            if (sidString == "S-1-5-10")
                                                continue;
                                            if (resultDict.ContainsKey(sid))
                                                resultDict[sid].HasSendAsPermission(oaRule);
                                            else
                                            {
                                                mailboxPermission = new MailboxPermission(mailboxGuid, rootPath);
                                                if (mailboxPermission.HasSendAsPermission(oaRule))
                                                    resultDict.Add(sid, mailboxPermission);
                                            }
                                        }
                                    }
                                }
                                catch (Exception ex)
                                {
                                    throw new InvalidOperationException(String.Format(
                                        "Error processing nTSecurityDescriptor attribute in object {0}", mailboxGuid), ex);
                                }
                            }
                            else
                                Debug.WriteLine(String.Format("Null nTSecurityDescriptor attribute in object {0}", mailboxGuid.ToString()));

                            // Merge current resultDict in dict
                            foreach (var kvp in resultDict)
                            {
                                if (!dict.ContainsKey(kvp.Key))
                                    dict[kvp.Key] = new List<MailboxPermission>();
                                dict[kvp.Key].Add(kvp.Value);
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException(String.Format(
                    "Error indexing mailbox permissions in path {0}", rootPath), ex);
            }
            Debug.Print("Finished index for mailbox permissions in {0}", rootPath);
            return dict;
        }

        public List<MailboxPermission> GetUserPermissions(SecurityIdentifier sid)
        {
            List<MailboxPermission> result = new List<MailboxPermission>();
            foreach (string key in _sidDict.Keys)
                if (_sidDict[key].ContainsKey(sid))
                    result.AddRange(_sidDict[key][sid]);

            return result;
        }

        public void NoOp()
        { }

    }
}
